<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>绘制名片</title>
    <style>
        body {
            display: flex;
            width: 100%;
            justify-content: center;
        }

        #myCanvas {
            margin: 300px;
            border-radius: 20px;
            box-shadow: 0 0 10px #E9EAEB;
        }
    </style>
</head>
<body>
<canvas id="myCanvas"></canvas>
</body>
<script>
    // 获取节点
    const canvas = document.querySelector('#myCanvas')
    // 创建上下文绘图环境
    const context = canvas.getContext("2d")

    // 设置 canvas 元素宽高
    const width = 540, height = 325
    canvas.style.width = `${width}px`
    canvas.style.height = `${height}px`

    /**
     * devicePixelRatio：像素比 (物理像素 / css像素)
     * 即 css 的 1px 对应几个物理像素
     * */
    const ratio = window.devicePixelRatio

    // 将画布宽高设置为 像素比乘以元素宽高
    canvas.width = Math.floor(ratio * width)
    canvas.height = Math.floor(ratio * height)

    // 将绘图环境按像素比缩放
    context.scale(ratio, ratio)

    // 绘制配置项
    const options = [
        // 画布圆角背景
        {
            type: 'rect',
            // 绘制起始坐标为画布起始坐标
            position: {
                top: 0,
                left: 0,
            },
            size: { width, height },
            // 圆角大小与 canvas 标签圆角保持一致
            round: 20,
            // 裁剪掉圆角以外区域
            isClip: true
        },
        // 左侧图片
        {
            type: 'image',
            imgUrl: './img/home.jpg',
            // 图片在 x 轴的偏移, 正值左移, 负值右移
            cropImgX: 130,
            // 图片在 y 轴的偏移, 正值下移, 负值上移
            cropImgY: 0,
            position: {
                top: 0,
                left: 0
            },
            size: {
                width: 140,
                height: 325
            }
        },
        // 右侧矩形
        {
            type: 'rect',
            position: {
                top: 0,
                left: 140,
            },
            size: {
                width: 400,
                height: 325,
            },
            background: '#F5F5FB'
        },
        // 中文公司名
        {
            type: 'text',
            position: {
                top: 40,
                left: 170
            },
            content: '成都没有科技有限公司',
            style: {
                fontSize: 24,
                color: '#999999'
            }
        },
        // 英文公司名
        {
            type: 'text',
            position: {
                top: 75,
                left: 170
            },
            content: 'Chengdu No Science And Technology Co., Ltd',
            style: {
                fontSize: 15,
                color: '#999999'
            }
        },
        // 姓名
        {
            type: 'text',
            position: {
                top: 235,
                left: 170
            },
            content: '王二狗',
            style: {
                fontSize: 24,
                color: '#999999',
                bold: true
            }
        },
        // 岗位
        {
            type: 'text',
            position: {
                top: 280,
                left: 170
            },
            content: '高级CV开发工程师',
            style: {
                fontSize: 16,
                color: '#999999'
            }
        },
        // 右上角线条
        {
            type: 'line',
            // 线条样式
            style: {
                width: 1,
                color: '#c3c3c3'
            },
            // 起始坐标
            startPoint: {
                x: 490,
                y: 30
            },
            // 路径坐标
            routePoint: [
                { x: 510, y: 30 },
                { x: 510, y: 50 }
            ]
        },
        // 右下角二维码
        {
            type: 'image',
            imgUrl: './img/code.png',
            position: {
                top: 220,
                left: 435
            },
            size: {
                width: 80,
                height: 80
            }
        }
    ]

    drawCanvas(options)

    /**
     * @description 调用绘制函数
     *
     * @param {array} options 绘制列表数组
     * */
    function drawCanvas(options) {
        options.forEach(item => {
            // 根据的数组项的 type 类型决定调用的绘制函数
            switch (item.type) {
                case 'rect':
                    rect(item)
                    break
                case 'image':
                    image(item)
                    break
                case 'text':
                    text(item)
                    break
                case 'line':
                    line(item)
                    break
                // 指定类型错误或未指定类型则抛错
                default:
                    throw new Error('存在错误的类型 type')
            }
        })
    }

    /**
     * @description 绘制矩形
     *
     * @param {{ left, top }} position 矩形左边距和上边距
     * @param {{ width, height }} size 矩形宽度高度
     * @param {{ width, color }} border 矩形边框宽度颜色
     * @param { string } background 矩形背景色, 默认白色
     * @param { number } round 矩形圆角, 默认无圆角
     * @param { boolean } isClip 是否裁剪矩形外区域, 默认不裁剪
     * */
    function rect({ position, size, border = {}, background = 'white', round = 0, isClip = false }) {
        // 最终的 x / y 坐标
        let rectX = position.left, rectY = position.top
        // 最终的矩形宽度 / 高度
        let rectWidth = size.width, rectHeight = size.height

        // 边框宽度
        const roundWidth = border.width || 0
        /**
         * 边框颜色
         * 未设置边框宽度颜色透明
         * 设置宽度未设置颜色使用背景色
         * */
        const roundColor = roundWidth ? (border.color ? border.color : background) : 'transparent'
        // 圆角最小值 0，最大值为矩形宽度的 1/2
        if (round > rectWidth / 2) round = rectWidth / 2
        if (round < 0) round = 0
        // 设置边框宽度
        context.lineWidth = roundWidth
        // 因为 canvas 线条是从中线向两边延伸，所以矩形绘制点在边框的中间，需要取边框宽度的一半
        const roundHalf = roundWidth / 2
        // 开始绘制
        context.beginPath()
        // 起始点 (有圆角的起始点 x 坐标需要加上边框的一半)
        context.moveTo(round ? rectX + round + roundHalf : rectX + round, rectY + roundHalf)
        // 绘制的四个点
        const [rTPoint, rBPoint, lBPoint, lTPoint] = [
            { x: rectWidth + rectX - roundHalf, y: rectY + roundHalf },
            { x: rectWidth + rectX - roundHalf, y: rectHeight + rectY - roundHalf },
            { x: rectX + roundHalf, y: rectHeight + rectY - roundHalf },
            { x: rectX + roundHalf, y: rectY + roundHalf }
        ]
        // 绘制四条切线和和圆角
        context.arcTo(rTPoint.x, rTPoint.y, rBPoint.x, rBPoint.y, round)
        context.arcTo(rBPoint.x, rBPoint.y, lBPoint.x, lBPoint.y, round)
        context.arcTo(lBPoint.x, lBPoint.y, lTPoint.x, lTPoint.y, round)
        context.arcTo(lTPoint.x, lTPoint.y, rTPoint.x, rTPoint.y, round)
        // 裁剪
        if (isClip) context.clip()
        // 绘制边框颜色
        context.strokeStyle = roundColor
        context.stroke()
        // 绘制填充颜色
        context.fillStyle = background
        context.fill()
        // 结束绘制
        context.closePath()
    }

    /**
     * @description 绘制图片
     *
     * @param {string} imgUrl 图片路径
     * @param {number} cropImgX 裁剪图片的起始 x 坐标, 默认 0
     * @param {number} cropImgY 裁剪图片的起始 y 坐标, 默认 0
     * @param {{ left, top }} position 绘制图片的起始坐标
     * @param {{ width, height }} size 绘制图片的宽度高度
     * */
    function image({ imgUrl, cropImgX = 0, cropImgY = 0, position: { left, top }, size: { width, height } }) {
        // 实例化 img 对象
        const img = new Image()
        // 设置跨域
        img.setAttribute('crossOrigin', 'anonymous')
        // 设置图片地址
        img.src = imgUrl
        // 图片加载完成
        img.onload = () => {
            // 如果未设置裁剪图片宽度高度则使用图片自身高度宽度
            let imgWidth = img.width
            let imgHeight = img.height

            let drawWidth = imgWidth
            // 如果不是正方形，则先按高度等比缩放计算绘制宽度
            if (imgWidth !== imgHeight) drawWidth = Math.floor(imgWidth / imgHeight * height)
            // 绘制图片
            context.drawImage(img, cropImgX, cropImgY, drawWidth, imgHeight, left, top, width, height)
        }
    }

    /**
     * @description 绘制文本
     *
     * @param {string} content 文本内容
     * @param {{ fontSize, color, bold }} style 文本大小 [默认 14]、颜色 [默认黑色]、加粗 [默认不加粗]
     * @param {{ left, top }} position 文本左边距和上边距
     * */
    function text({ content, style: { fontSize = 14, color = 'black', bold = false }, position }) {
        // 文本加粗、大小、字体
        context.font = `${bold ? 'bold ' : ''}${fontSize}px YaHei`
        // 文本颜色
        context.fillStyle = color
        // 文本对齐方式
        context.textBaseline = 'top'
        // 绘制文本
        context.fillText(content, position.left, position.top)
    }

    /**
     * @description 绘制线条
     *
     * @param {{ width, color }} style 线条宽度[默认 1]、颜色[默认黑色]
     * @param {{ x, y }} startPoint 起始点坐标
     * @param {[{ x, y }]} routePoint 路径点坐标集合
     * */
    function line({ style: { width = 1, color = 'black' }, startPoint, routePoint }) {
        // 开始绘制
        context.beginPath()
        // 线条大小
        context.lineWidth = width
        // 线条颜色
        context.strokeStyle = color
        // 起始点
        context.moveTo(startPoint.x, startPoint.y)
        // 遍历绘制路径点
        routePoint.forEach(({ x, y }) => context.lineTo(x, y))
        // 绘制线条
        context.stroke()
        // 结束绘制
        context.closePath()
    }

    // 监听 canvas 点击事件
    canvas.addEventListener('click', () => {
        // 创建 a 标签
        const a = document.createElement('a')
        // 设置 a 链接为图片地址
        a.href = canvas.toDataURL()
        // 设置图片名称, 默认类型 png
        a.download = new Date().toLocaleTimeString()
        // 触发点击事件
        a.click()
    })
</script>
</html>
